"""
02_nodes_and_properties.py
==========================
Nodes, Labels, and Properties in Neo4j

This module covers:
1. Creating nodes with labels
2. Adding and modifying properties
3. Property data types
4. Multi-label nodes
5. Best practices for node design

Author: Neo4j Tutorial
Date: January 2025
"""

from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field, asdict
from datetime import datetime, date
from enum import Enum
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.utils.connection import get_driver


# ============================================================
# DATA MODELS
# ============================================================

@dataclass
class Patient:
    """Patient entity model."""
    id: str
    name: str
    date_of_birth: date
    gender: str
    blood_type: Optional[str] = None
    allergies: List[str] = field(default_factory=list)
    active: bool = True
    created_at: datetime = field(default_factory=datetime.now)


@dataclass
class Disease:
    """Disease entity model using ICD-11 codes."""
    id: str  # ICD-11 code
    name: str
    category: str
    description: Optional[str] = None
    severity: Optional[str] = None  # mild, moderate, severe


@dataclass
class Medication:
    """Medication entity model using RxNorm."""
    id: str  # RxNorm code
    name: str
    generic_name: str
    drug_class: str
    dosage_forms: List[str] = field(default_factory=list)


class PropertyType(Enum):
    """Supported Neo4j property types."""
    STRING = "String"
    INTEGER = "Integer"
    FLOAT = "Float"
    BOOLEAN = "Boolean"
    DATE = "Date"
    DATETIME = "DateTime"
    POINT = "Point"
    LIST = "List"


# ============================================================
# NODE MANAGER
# ============================================================

class NodeManager:
    """
    Manages node operations in Neo4j.
    
    Node Structure in Neo4j:
    
    ┌─────────────────────────────────────────────┐
    │  NODE                                       │
    ├─────────────────────────────────────────────┤
    │  Labels: [:Patient, :VIP]                   │
    │                                             │
    │  Properties:                                │
    │    id: "P001"                              │
    │    name: "John Doe"                        │
    │    dateOfBirth: date("1980-05-15")         │
    │    bloodType: "O+"                         │
    │    allergies: ["Penicillin", "Latex"]      │
    │    active: true                            │
    │    createdAt: datetime()                   │
    └─────────────────────────────────────────────┘
    """
    
    def __init__(self, driver=None):
        self.driver = driver or get_driver()
    
    # =========================================================
    # CREATE OPERATIONS
    # =========================================================
    
    def create_patient(self, patient: Patient) -> Dict[str, Any]:
        """
        Create a Patient node.
        
        Args:
            patient: Patient dataclass instance
            
        Returns:
            Created node data
        """
        query = """
        CREATE (p:Patient {
            id: $id,
            name: $name,
            dateOfBirth: date($dob),
            gender: $gender,
            bloodType: $blood_type,
            allergies: $allergies,
            active: $active,
            createdAt: datetime()
        })
        RETURN p
        """
        
        with self.driver.session() as session:
            result = session.run(
                query,
                id=patient.id,
                name=patient.name,
                dob=patient.date_of_birth.isoformat(),
                gender=patient.gender,
                blood_type=patient.blood_type,
                allergies=patient.allergies,
                active=patient.active
            )
            record = result.single()
            return dict(record["p"]) if record else None
    
    def create_disease(self, disease: Disease) -> Dict[str, Any]:
        """Create a Disease node."""
        query = """
        CREATE (d:Disease {
            id: $id,
            name: $name,
            category: $category,
            description: $description,
            severity: $severity,
            createdAt: datetime()
        })
        RETURN d
        """
        
        with self.driver.session() as session:
            result = session.run(
                query,
                id=disease.id,
                name=disease.name,
                category=disease.category,
                description=disease.description,
                severity=disease.severity
            )
            record = result.single()
            return dict(record["d"]) if record else None
    
    def create_medication(self, medication: Medication) -> Dict[str, Any]:
        """Create a Medication node."""
        query = """
        CREATE (m:Medication {
            id: $id,
            name: $name,
            genericName: $generic_name,
            drugClass: $drug_class,
            dosageForms: $dosage_forms,
            createdAt: datetime()
        })
        RETURN m
        """
        
        with self.driver.session() as session:
            result = session.run(
                query,
                id=medication.id,
                name=medication.name,
                generic_name=medication.generic_name,
                drug_class=medication.drug_class,
                dosage_forms=medication.dosage_forms
            )
            record = result.single()
            return dict(record["m"]) if record else None
    
    def create_node_generic(
        self,
        labels: List[str],
        properties: Dict[str, Any]
    ) -> Dict[str, Any]:
        """
        Create a node with any labels and properties.
        
        Args:
            labels: List of node labels
            properties: Dictionary of properties
            
        Returns:
            Created node data
        """
        labels_str = ":".join(labels)
        prop_keys = list(properties.keys())
        prop_str = ", ".join([f"{k}: ${k}" for k in prop_keys])
        
        query = f"""
        CREATE (n:{labels_str} {{{prop_str}}})
        SET n.createdAt = datetime()
        RETURN n
        """
        
        with self.driver.session() as session:
            result = session.run(query, **properties)
            record = result.single()
            return dict(record["n"]) if record else None
    
    # =========================================================
    # BULK CREATE
    # =========================================================
    
    def bulk_create_patients(
        self,
        patients: List[Patient],
        batch_size: int = 1000
    ) -> int:
        """
        Bulk create patients using UNWIND.
        
        UNWIND is efficient for batch operations.
        
        Args:
            patients: List of Patient objects
            batch_size: Batch size for processing
            
        Returns:
            Number of patients created
        """
        query = """
        UNWIND $patients AS p
        CREATE (patient:Patient {
            id: p.id,
            name: p.name,
            dateOfBirth: date(p.dob),
            gender: p.gender,
            bloodType: p.blood_type,
            allergies: p.allergies,
            active: p.active,
            createdAt: datetime()
        })
        RETURN count(patient) as created
        """
        
        # Convert to dictionaries
        patient_data = [
            {
                "id": p.id,
                "name": p.name,
                "dob": p.date_of_birth.isoformat(),
                "gender": p.gender,
                "blood_type": p.blood_type,
                "allergies": p.allergies,
                "active": p.active
            }
            for p in patients
        ]
        
        total_created = 0
        
        with self.driver.session() as session:
            # Process in batches
            for i in range(0, len(patient_data), batch_size):
                batch = patient_data[i:i + batch_size]
                result = session.run(query, patients=batch)
                total_created += result.single()["created"]
        
        return total_created
    
    # =========================================================
    # READ OPERATIONS
    # =========================================================
    
    def get_patient(self, patient_id: str) -> Optional[Dict[str, Any]]:
        """Get patient by ID."""
        query = """
        MATCH (p:Patient {id: $id})
        RETURN p
        """
        
        with self.driver.session() as session:
            result = session.run(query, id=patient_id)
            record = result.single()
            return dict(record["p"]) if record else None
    
    def search_patients(
        self,
        name_contains: str = None,
        gender: str = None,
        active_only: bool = True,
        limit: int = 100
    ) -> List[Dict[str, Any]]:
        """
        Search patients with filters.
        
        Args:
            name_contains: Partial name match
            gender: Filter by gender
            active_only: Only active patients
            limit: Maximum results
            
        Returns:
            List of matching patients
        """
        conditions = []
        params = {"limit": limit}
        
        if name_contains:
            conditions.append("toLower(p.name) CONTAINS toLower($name)")
            params["name"] = name_contains
        
        if gender:
            conditions.append("p.gender = $gender")
            params["gender"] = gender
        
        if active_only:
            conditions.append("p.active = true")
        
        where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
        
        query = f"""
        MATCH (p:Patient)
        {where_clause}
        RETURN p
        ORDER BY p.name
        LIMIT $limit
        """
        
        with self.driver.session() as session:
            result = session.run(query, **params)
            return [dict(record["p"]) for record in result]
    
    # =========================================================
    # UPDATE OPERATIONS
    # =========================================================
    
    def update_patient_properties(
        self,
        patient_id: str,
        properties: Dict[str, Any]
    ) -> Optional[Dict[str, Any]]:
        """
        Update patient properties.
        
        Uses SET to update or add properties.
        """
        set_clauses = [f"p.{k} = ${k}" for k in properties.keys()]
        set_clauses.append("p.updatedAt = datetime()")
        set_str = ", ".join(set_clauses)
        
        query = f"""
        MATCH (p:Patient {{id: $patient_id}})
        SET {set_str}
        RETURN p
        """
        
        params = {"patient_id": patient_id, **properties}
        
        with self.driver.session() as session:
            result = session.run(query, **params)
            record = result.single()
            return dict(record["p"]) if record else None
    
    def add_label(
        self,
        node_label: str,
        node_id: str,
        new_label: str
    ) -> bool:
        """Add a label to an existing node."""
        query = f"""
        MATCH (n:{node_label} {{id: $id}})
        SET n:{new_label}
        RETURN n
        """
        
        with self.driver.session() as session:
            result = session.run(query, id=node_id)
            return result.single() is not None
    
    def remove_label(
        self,
        node_label: str,
        node_id: str,
        label_to_remove: str
    ) -> bool:
        """Remove a label from a node."""
        query = f"""
        MATCH (n:{node_label} {{id: $id}})
        REMOVE n:{label_to_remove}
        RETURN n
        """
        
        with self.driver.session() as session:
            result = session.run(query, id=node_id)
            return result.single() is not None
    
    # =========================================================
    # DELETE OPERATIONS
    # =========================================================
    
    def delete_patient(self, patient_id: str, detach: bool = True) -> bool:
        """
        Delete a patient node.
        
        Args:
            patient_id: Patient ID
            detach: If True, also delete relationships
            
        Returns:
            True if deleted
        """
        if detach:
            query = """
            MATCH (p:Patient {id: $id})
            DETACH DELETE p
            RETURN count(*) as deleted
            """
        else:
            query = """
            MATCH (p:Patient {id: $id})
            DELETE p
            RETURN count(*) as deleted
            """
        
        with self.driver.session() as session:
            result = session.run(query, id=patient_id)
            return result.single()["deleted"] > 0
    
    def delete_all_by_label(self, label: str) -> int:
        """Delete all nodes with a specific label."""
        query = f"""
        MATCH (n:{label})
        DETACH DELETE n
        RETURN count(*) as deleted
        """
        
        with self.driver.session() as session:
            result = session.run(query)
            return result.single()["deleted"]


# ============================================================
# DEMONSTRATION
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("Nodes and Properties - Demonstration")
    print("=" * 60)
    
    try:
        manager = NodeManager()
        
        # Create patients
        print("\n📌 Creating Patient Nodes")
        print("-" * 40)
        
        patients = [
            Patient(
                id="P001",
                name="John Doe",
                date_of_birth=date(1980, 5, 15),
                gender="Male",
                blood_type="O+",
                allergies=["Penicillin"]
            ),
            Patient(
                id="P002",
                name="Jane Smith",
                date_of_birth=date(1992, 8, 22),
                gender="Female",
                blood_type="A-",
                allergies=[]
            ),
            Patient(
                id="P003",
                name="Bob Wilson",
                date_of_birth=date(1975, 12, 1),
                gender="Male",
                blood_type="B+",
                allergies=["Latex", "Sulfa"]
            )
        ]
        
        for patient in patients:
            result = manager.create_patient(patient)
            if result:
                print(f"   Created: {result['name']} (ID: {result['id']})")
        
        # Create diseases
        print("\n📌 Creating Disease Nodes")
        print("-" * 40)
        
        diseases = [
            Disease(
                id="E11",
                name="Type 2 Diabetes Mellitus",
                category="Endocrine",
                severity="moderate"
            ),
            Disease(
                id="I10",
                name="Essential Hypertension",
                category="Cardiovascular",
                severity="mild"
            ),
            Disease(
                id="J45",
                name="Asthma",
                category="Respiratory",
                severity="moderate"
            )
        ]
        
        for disease in diseases:
            result = manager.create_disease(disease)
            if result:
                print(f"   Created: {result['name']} (Code: {result['id']})")
        
        # Create medications
        print("\n📌 Creating Medication Nodes")
        print("-" * 40)
        
        medications = [
            Medication(
                id="6809",
                name="Metformin",
                generic_name="Metformin Hydrochloride",
                drug_class="Biguanide",
                dosage_forms=["Tablet", "Extended-release tablet"]
            ),
            Medication(
                id="29046",
                name="Lisinopril",
                generic_name="Lisinopril",
                drug_class="ACE Inhibitor",
                dosage_forms=["Tablet"]
            )
        ]
        
        for med in medications:
            result = manager.create_medication(med)
            if result:
                print(f"   Created: {result['name']} (RxNorm: {result['id']})")
        
        # Search patients
        print("\n📌 Searching Patients")
        print("-" * 40)
        results = manager.search_patients(name_contains="o")
        for p in results:
            print(f"   Found: {p['name']}")
        
        # Update patient
        print("\n📌 Updating Patient Properties")
        print("-" * 40)
        updated = manager.update_patient_properties(
            "P001",
            {"bloodType": "O-", "allergies": ["Penicillin", "Aspirin"]}
        )
        if updated:
            print(f"   Updated: {updated['name']}")
            print(f"   New blood type: {updated['bloodType']}")
            print(f"   Allergies: {updated['allergies']}")
        
        print("\n" + "=" * 60)
        print("✅ Nodes and properties demonstration complete!")
        print("=" * 60)
        
    except Exception as e:
        print(f"Error: {e}")
